starting list/tag support in TLs

This commit is contained in:
Nicolas Constant 2018-09-16 02:09:48 -04:00
parent 756e36f2f7
commit c5cd5e8f7a
No known key found for this signature in database
GPG Key ID: 1E9F677FB01A5688
5 changed files with 155 additions and 99 deletions

View File

@ -2,11 +2,10 @@ import { Component, OnInit, Input } from "@angular/core";
import { AccountWrapper } from "../../models/account.models"; import { AccountWrapper } from "../../models/account.models";
import { StreamElement, StreamTypeEnum } from "../../states/streams.state"; import { StreamElement, StreamTypeEnum } from "../../states/streams.state";
import { StreamingService, StreamingWrapper, EventEnum, StatusUpdate } from "../../services/streaming.service"; import { StreamingService, StreamingWrapper, EventEnum, StatusUpdate } from "../../services/streaming.service";
import { HttpClient, HttpHeaders } from "@angular/common/http";
import { Store } from "@ngxs/store"; import { Store } from "@ngxs/store";
import { AccountInfo } from "../../states/accounts.state"; import { AccountInfo } from "../../states/accounts.state";
import { ApiRoutes } from "../../services/models/api.settings";
import { Status } from "../../services/models/mastodon.interfaces"; import { Status } from "../../services/models/mastodon.interfaces";
import { MastodonService } from "../../services/mastodon.service";
@Component({ @Component({
selector: "app-stream", selector: "app-stream",
@ -15,7 +14,6 @@ import { Status } from "../../services/models/mastodon.interfaces";
}) })
export class StreamComponent implements OnInit { export class StreamComponent implements OnInit {
private _streamElement: StreamElement; private _streamElement: StreamElement;
private apiRoutes = new ApiRoutes();
private account: AccountInfo; private account: AccountInfo;
private websocketStreaming: StreamingWrapper; private websocketStreaming: StreamingWrapper;
@ -41,7 +39,7 @@ export class StreamComponent implements OnInit {
constructor( constructor(
private readonly store: Store, private readonly store: Store,
private readonly streamingService: StreamingService, private readonly streamingService: StreamingService,
private readonly httpClient: HttpClient) { private readonly mastodonService: MastodonService) {
} }
ngOnInit() { ngOnInit() {
@ -51,28 +49,13 @@ export class StreamComponent implements OnInit {
return false; return false;
} }
private getTimelineRoute(): string {
switch (this._streamElement.type) {
case StreamTypeEnum.personnal:
return this.apiRoutes.getHomeTimeline;
case StreamTypeEnum.local:
return this.apiRoutes.getPublicTimeline + `?Local=true`;
case StreamTypeEnum.global:
return this.apiRoutes.getPublicTimeline + `?Local=false`;
}
}
private getRegisteredAccounts(): AccountInfo[] { private getRegisteredAccounts(): AccountInfo[] {
var regAccounts = <AccountInfo[]>this.store.snapshot().registeredaccounts.accounts; var regAccounts = <AccountInfo[]>this.store.snapshot().registeredaccounts.accounts;
return regAccounts; return regAccounts;
} }
private retrieveToots(): void { private retrieveToots(): void {
const route = `https://${this.account.instance}${this.getTimelineRoute()}`; this.mastodonService.getTimeline(this.account, this._streamElement.type)
const headers = new HttpHeaders({ 'Authorization': `Bearer ${this.account.token.access_token}` });
this.httpClient.get<Status[]>(route, { headers: headers }).toPromise()
.then((results: Status[]) => { .then((results: Status[]) => {
for (const s of results) { for (const s of results) {
this.statuses.push(s); this.statuses.push(s);
@ -81,21 +64,7 @@ export class StreamComponent implements OnInit {
} }
private launchWebsocket(): void { private launchWebsocket(): void {
//Web socket this.websocketStreaming = this.streamingService.getStreaming(this.account.instance, this.account.token.access_token, this._streamElement.type);
let streamRequest: string;
switch (this._streamElement.type) {
case StreamTypeEnum.global:
streamRequest = 'public';
break;
case StreamTypeEnum.local:
streamRequest = 'public:local';
break;
case StreamTypeEnum.personnal:
streamRequest = 'user';
break;
}
this.websocketStreaming = this.streamingService.getStreaming(this.account.instance, this.account.token.access_token, streamRequest);
this.websocketStreaming.statusUpdateSubjet.subscribe((update: StatusUpdate) => { this.websocketStreaming.statusUpdateSubjet.subscribe((update: StatusUpdate) => {
if (update) { if (update) {
if (update.type === EventEnum.update) { if (update.type === EventEnum.update) {

View File

@ -2,17 +2,69 @@ import { Injectable } from '@angular/core';
import { HttpHeaders, HttpClient } from '@angular/common/http'; import { HttpHeaders, HttpClient } from '@angular/common/http';
import { ApiRoutes } from './models/api.settings'; import { ApiRoutes } from './models/api.settings';
import { Account } from "./models/mastodon.interfaces"; import { Account, Status } from "./models/mastodon.interfaces";
import { AccountInfo } from '../states/accounts.state'; import { AccountInfo } from '../states/accounts.state';
import { StreamTypeEnum } from '../states/streams.state';
@Injectable() @Injectable()
export class MastodonService { export class MastodonService {
private apiRoutes = new ApiRoutes();
constructor(private readonly httpClient: HttpClient) {} private apiRoutes = new ApiRoutes();
retrieveAccountDetails(account: AccountInfo): Promise<Account> { constructor(private readonly httpClient: HttpClient) { }
const headers = new HttpHeaders({'Authorization':`Bearer ${account.token.access_token}`});
return this.httpClient.get<Account>('https://' + account.instance + this.apiRoutes.getCurrentAccount, {headers: headers}).toPromise(); retrieveAccountDetails(account: AccountInfo): Promise<Account> {
} const headers = new HttpHeaders({ 'Authorization': `Bearer ${account.token.access_token}` });
return this.httpClient.get<Account>('https://' + account.instance + this.apiRoutes.getCurrentAccount, { headers: headers }).toPromise();
}
getTimeline(account: AccountInfo, type: StreamTypeEnum, max_id: string = null, since_id: string = null, limit: number = 20): Promise<Status[]> {
const route = `https://${account.instance}${this.getTimelineRoute(type, max_id, since_id, limit)}`;
const headers = new HttpHeaders({ 'Authorization': `Bearer ${account.token.access_token}` });
return this.httpClient.get<Status[]>(route, { headers: headers }).toPromise()
}
private getTimelineRoute(type: StreamTypeEnum, max_id: string, since_id: string, limit: number): string {
let route: string;
switch (type) {
case StreamTypeEnum.personnal:
route = this.apiRoutes.getHomeTimeline;
break;
case StreamTypeEnum.local:
route = this.apiRoutes.getPublicTimeline + `?local=true&`;
break;
case StreamTypeEnum.global:
route = this.apiRoutes.getPublicTimeline + `?local=false&`;
break;
case StreamTypeEnum.directmessages:
route = this.apiRoutes.getDirectTimeline;
break;
case StreamTypeEnum.tag:
route = this.apiRoutes.getTagTimeline.replace('{0}', 'TODO');
break;
case StreamTypeEnum.list:
route = this.apiRoutes.getListTimeline.replace('{0}', 'TODO');
break;
default:
throw new Error('StreamTypeEnum not supported');
}
if (!route.includes('?')) route = route + '?';
if (max_id) route = route + `max_id=${max_id}&`;
if (since_id) route = route + `since_id=${since_id}&`;
if (limit) route = route + `limit=${limit}&`;
return this.trimChar(this.trimChar(route, '?'), '&');
}
private escapeRegExp = function(strToEscape) {
// Escape special characters for use in a regular expression
return strToEscape.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
};
private trimChar = function(origString, charToTrim) {
charToTrim = this.escapeRegExp(charToTrim);
var regEx = new RegExp("^[" + charToTrim + "]+|[" + charToTrim + "]+$", "g");
return origString.replace(regEx, "");
};
} }

View File

@ -1,47 +1,50 @@
export class ApiRoutes { export class ApiRoutes {
createApp = "/api/v1/apps"; createApp = '/api/v1/apps';
getToken = "/oauth/token"; getToken = '/oauth/token';
getAccount = "/api/v1/accounts/{0}"; getAccount = '/api/v1/accounts/{0}';
getCurrentAccount = "/api/v1/accounts/verify_credentials"; getCurrentAccount = '/api/v1/accounts/verify_credentials';
getAccountFollowers = "/api/v1/accounts/{0}/followers"; getAccountFollowers = '/api/v1/accounts/{0}/followers';
getAccountFollowing = "/api/v1/accounts/{0}/following"; getAccountFollowing = '/api/v1/accounts/{0}/following';
getAccountStatuses = "/api/v1/accounts/{0}/statuses"; getAccountStatuses = '/api/v1/accounts/{0}/statuses';
follow = "/api/v1/accounts/{0}/follow"; follow = '/api/v1/accounts/{0}/follow';
unfollow = "/api/v1/accounts/{0}/unfollow"; unfollow = '/api/v1/accounts/{0}/unfollow';
block = "/api/v1/accounts/{0}/block"; block = '/api/v1/accounts/{0}/block';
unblock = "/api/v1/accounts/{0}/unblock"; unblock = '/api/v1/accounts/{0}/unblock';
mute = "/api/v1/accounts/{0}/mute"; mute = '/api/v1/accounts/{0}/mute';
unmute = "/api/v1/accounts/{0}/unmute"; unmute = '/api/v1/accounts/{0}/unmute';
getAccountRelationships = "/api/v1/accounts/relationships"; getAccountRelationships = '/api/v1/accounts/relationships';
searchForAccounts = "/api/v1/accounts/search"; searchForAccounts = '/api/v1/accounts/search';
getBlocks = "/api/v1/blocks"; getBlocks = '/api/v1/blocks';
getFavourites = "/api/v1/favourites"; getFavourites = '/api/v1/favourites';
getFollowRequests = "/api/v1/follow_requests"; getFollowRequests = '/api/v1/follow_requests';
authorizeFollowRequest = "/api/v1/follow_requests/authorize"; authorizeFollowRequest = '/api/v1/follow_requests/authorize';
rejectFollowRequest = "/api/v1/follow_requests/reject"; rejectFollowRequest = '/api/v1/follow_requests/reject';
followRemote = "/api/v1/follows"; followRemote = '/api/v1/follows';
getInstance = "/api/v1/instance"; getInstance = '/api/v1/instance';
uploadMediaAttachment = "/api/v1/media"; uploadMediaAttachment = '/api/v1/media';
getMutes = "/api/v1/mutes"; getMutes = '/api/v1/mutes';
getNotifications = "/api/v1/notifications"; getNotifications = '/api/v1/notifications';
getSingleNotifications = "/api/v1/notifications/{0}"; getSingleNotifications = '/api/v1/notifications/{0}';
clearNotifications = "/api/v1/notifications/clear"; clearNotifications = '/api/v1/notifications/clear';
getReports = "/api/v1/reports"; getReports = '/api/v1/reports';
reportUser = "/api/v1/reports"; reportUser = '/api/v1/reports';
search = "/api/v1/search"; search = '/api/v1/search';
getStatus = "/api/v1/statuses/{0}"; getStatus = '/api/v1/statuses/{0}';
getStatusContext = "/api/v1/statuses/{0}/context"; getStatusContext = '/api/v1/statuses/{0}/context';
getStatusCard = "/api/v1/statuses/{0}/card"; getStatusCard = '/api/v1/statuses/{0}/card';
getStatusRebloggedBy = "/api/v1/statuses/{0}/reblogged_by"; getStatusRebloggedBy = '/api/v1/statuses/{0}/reblogged_by';
getStatusFavouritedBy = "/api/v1/statuses/{0}/favourited_by"; getStatusFavouritedBy = '/api/v1/statuses/{0}/favourited_by';
postNewStatus = "/api/v1/statuses"; postNewStatus = '/api/v1/statuses';
deleteStatus = "/api/v1/statuses/{0}"; deleteStatus = '/api/v1/statuses/{0}';
reblogStatus = "/api/v1/statuses/{0}/reblog"; reblogStatus = '/api/v1/statuses/{0}/reblog';
unreblogStatus = "/api/v1/statuses/{0}/unreblog"; unreblogStatus = '/api/v1/statuses/{0}/unreblog';
favouritingStatus = "/api/v1/statuses/{0}/favourite"; favouritingStatus = '/api/v1/statuses/{0}/favourite';
unfavouritingStatus = "/api/v1/statuses/{0}/unfavourite"; unfavouritingStatus = '/api/v1/statuses/{0}/unfavourite';
getHomeTimeline = "/api/v1/timelines/home"; getHomeTimeline = '/api/v1/timelines/home';
getPublicTimeline = "/api/v1/timelines/public"; getPublicTimeline = '/api/v1/timelines/public';
getHastagTimeline = "/api/v1/timelines/tag/{0}"; getHastagTimeline = '/api/v1/timelines/tag/{0}';
getDirectTimeline = '/api/v1/timelines/direct';
getTagTimeline = '/api/v1/timelines/tag/{0}';
getListTimeline = '/api/v1/timelines/list/{0}';
} }

View File

@ -2,6 +2,7 @@ import { Injectable } from "@angular/core";
import { Status } from "./models/mastodon.interfaces"; import { Status } from "./models/mastodon.interfaces";
import { BehaviorSubject } from "rxjs"; import { BehaviorSubject } from "rxjs";
import { ApiRoutes } from "./models/api.settings"; import { ApiRoutes } from "./models/api.settings";
import { StreamTypeEnum } from "../states/streams.state";
@Injectable() @Injectable()
export class StreamingService { export class StreamingService {
@ -9,12 +10,22 @@ export class StreamingService {
constructor() { } constructor() { }
//TODO restructure this to handle real domain objects getStreaming(instance: string, accessToken: string, streamType: StreamTypeEnum): StreamingWrapper {
getStreaming(instance: string, accessToken: string, streamRequest: string): StreamingWrapper { const request = this.getRequest(streamType);
const route = `wss://${instance}/api/v1/streaming?access_token=${accessToken}&stream=${streamRequest}` const route = `wss://${instance}/api/v1/streaming?access_token=${accessToken}&stream=${request}`
return new StreamingWrapper(route); return new StreamingWrapper(route);
} }
private getRequest(type: StreamTypeEnum): string {
switch (type) {
case StreamTypeEnum.global:
return 'public';
case StreamTypeEnum.local:
return 'public:local';
case StreamTypeEnum.personnal:
return 'user';
}
}
} }
export class StreamingWrapper { export class StreamingWrapper {
@ -22,19 +33,37 @@ export class StreamingWrapper {
eventSource: WebSocket; eventSource: WebSocket;
constructor(private readonly domain: string) { constructor(private readonly domain: string) {
this.start(domain); this.start(domain);
} }
private start(domain: string) { private start(domain: string) {
this.eventSource = new WebSocket(domain); this.eventSource = new WebSocket(domain);
this.eventSource.onmessage = x => this.tootParsing(<WebSocketEvent>JSON.parse(x.data)); this.eventSource.onmessage = x => this.statusParsing(<WebSocketEvent>JSON.parse(x.data));
this.eventSource.onerror = x => console.error(x); this.eventSource.onerror = x => this.webSocketGotError(x);
this.eventSource.onopen = x => console.log(x); this.eventSource.onopen = x => console.log(x);
this.eventSource.onclose = x => { console.log(x); this.eventSource.onclose = x => this.webSocketClosed(domain, x);
setTimeout(() => {this.start(domain)}, 3000);}
} }
private tootParsing(event: WebSocketEvent) { private errorClosing: boolean;
private webSocketGotError(x: Event) {
console.error(x);
this.errorClosing = true;
// this.eventSource.close();
}
private webSocketClosed(domain, x: Event) {
console.log(x);
if(this.errorClosing){
this.errorClosing = false;
} else {
setTimeout(() => { this.start(domain) }, 3000);
}
}
private statusParsing(event: WebSocketEvent) {
const newUpdate = new StatusUpdate(); const newUpdate = new StatusUpdate();
switch (event.event) { switch (event.event) {
@ -52,6 +81,8 @@ export class StreamingWrapper {
this.statusUpdateSubjet.next(newUpdate); this.statusUpdateSubjet.next(newUpdate);
} }
} }
class WebSocketEvent { class WebSocketEvent {

View File

@ -40,5 +40,6 @@ export class StreamElement {
activity = 5, activity = 5,
list = 6, list = 6,
directmessages = 7, directmessages = 7,
} tag = 8,
}