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 { StreamElement, StreamTypeEnum } from "../../states/streams.state";
import { StreamingService, StreamingWrapper, EventEnum, StatusUpdate } from "../../services/streaming.service";
import { HttpClient, HttpHeaders } from "@angular/common/http";
import { Store } from "@ngxs/store";
import { AccountInfo } from "../../states/accounts.state";
import { ApiRoutes } from "../../services/models/api.settings";
import { Status } from "../../services/models/mastodon.interfaces";
import { MastodonService } from "../../services/mastodon.service";
@Component({
selector: "app-stream",
@ -15,7 +14,6 @@ import { Status } from "../../services/models/mastodon.interfaces";
})
export class StreamComponent implements OnInit {
private _streamElement: StreamElement;
private apiRoutes = new ApiRoutes();
private account: AccountInfo;
private websocketStreaming: StreamingWrapper;
@ -41,7 +39,7 @@ export class StreamComponent implements OnInit {
constructor(
private readonly store: Store,
private readonly streamingService: StreamingService,
private readonly httpClient: HttpClient) {
private readonly mastodonService: MastodonService) {
}
ngOnInit() {
@ -51,28 +49,13 @@ export class StreamComponent implements OnInit {
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[] {
var regAccounts = <AccountInfo[]>this.store.snapshot().registeredaccounts.accounts;
return regAccounts;
}
private retrieveToots(): void {
const route = `https://${this.account.instance}${this.getTimelineRoute()}`;
const headers = new HttpHeaders({ 'Authorization': `Bearer ${this.account.token.access_token}` });
this.httpClient.get<Status[]>(route, { headers: headers }).toPromise()
this.mastodonService.getTimeline(this.account, this._streamElement.type)
.then((results: Status[]) => {
for (const s of results) {
this.statuses.push(s);
@ -81,21 +64,7 @@ export class StreamComponent implements OnInit {
}
private launchWebsocket(): void {
//Web socket
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 = this.streamingService.getStreaming(this.account.instance, this.account.token.access_token, this._streamElement.type);
this.websocketStreaming.statusUpdateSubjet.subscribe((update: StatusUpdate) => {
if (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 { 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 { StreamTypeEnum } from '../states/streams.state';
@Injectable()
export class MastodonService {
private apiRoutes = new ApiRoutes();
constructor(private readonly httpClient: HttpClient) {}
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();
}
}
private apiRoutes = new ApiRoutes();
constructor(private readonly httpClient: HttpClient) { }
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 {
createApp = "/api/v1/apps";
getToken = "/oauth/token";
getAccount = "/api/v1/accounts/{0}";
getCurrentAccount = "/api/v1/accounts/verify_credentials";
getAccountFollowers = "/api/v1/accounts/{0}/followers";
getAccountFollowing = "/api/v1/accounts/{0}/following";
getAccountStatuses = "/api/v1/accounts/{0}/statuses";
follow = "/api/v1/accounts/{0}/follow";
unfollow = "/api/v1/accounts/{0}/unfollow";
block = "/api/v1/accounts/{0}/block";
unblock = "/api/v1/accounts/{0}/unblock";
mute = "/api/v1/accounts/{0}/mute";
unmute = "/api/v1/accounts/{0}/unmute";
getAccountRelationships = "/api/v1/accounts/relationships";
searchForAccounts = "/api/v1/accounts/search";
getBlocks = "/api/v1/blocks";
getFavourites = "/api/v1/favourites";
getFollowRequests = "/api/v1/follow_requests";
authorizeFollowRequest = "/api/v1/follow_requests/authorize";
rejectFollowRequest = "/api/v1/follow_requests/reject";
followRemote = "/api/v1/follows";
getInstance = "/api/v1/instance";
uploadMediaAttachment = "/api/v1/media";
getMutes = "/api/v1/mutes";
getNotifications = "/api/v1/notifications";
getSingleNotifications = "/api/v1/notifications/{0}";
clearNotifications = "/api/v1/notifications/clear";
getReports = "/api/v1/reports";
reportUser = "/api/v1/reports";
search = "/api/v1/search";
getStatus = "/api/v1/statuses/{0}";
getStatusContext = "/api/v1/statuses/{0}/context";
getStatusCard = "/api/v1/statuses/{0}/card";
getStatusRebloggedBy = "/api/v1/statuses/{0}/reblogged_by";
getStatusFavouritedBy = "/api/v1/statuses/{0}/favourited_by";
postNewStatus = "/api/v1/statuses";
deleteStatus = "/api/v1/statuses/{0}";
reblogStatus = "/api/v1/statuses/{0}/reblog";
unreblogStatus = "/api/v1/statuses/{0}/unreblog";
favouritingStatus = "/api/v1/statuses/{0}/favourite";
unfavouritingStatus = "/api/v1/statuses/{0}/unfavourite";
getHomeTimeline = "/api/v1/timelines/home";
getPublicTimeline = "/api/v1/timelines/public";
getHastagTimeline = "/api/v1/timelines/tag/{0}";
createApp = '/api/v1/apps';
getToken = '/oauth/token';
getAccount = '/api/v1/accounts/{0}';
getCurrentAccount = '/api/v1/accounts/verify_credentials';
getAccountFollowers = '/api/v1/accounts/{0}/followers';
getAccountFollowing = '/api/v1/accounts/{0}/following';
getAccountStatuses = '/api/v1/accounts/{0}/statuses';
follow = '/api/v1/accounts/{0}/follow';
unfollow = '/api/v1/accounts/{0}/unfollow';
block = '/api/v1/accounts/{0}/block';
unblock = '/api/v1/accounts/{0}/unblock';
mute = '/api/v1/accounts/{0}/mute';
unmute = '/api/v1/accounts/{0}/unmute';
getAccountRelationships = '/api/v1/accounts/relationships';
searchForAccounts = '/api/v1/accounts/search';
getBlocks = '/api/v1/blocks';
getFavourites = '/api/v1/favourites';
getFollowRequests = '/api/v1/follow_requests';
authorizeFollowRequest = '/api/v1/follow_requests/authorize';
rejectFollowRequest = '/api/v1/follow_requests/reject';
followRemote = '/api/v1/follows';
getInstance = '/api/v1/instance';
uploadMediaAttachment = '/api/v1/media';
getMutes = '/api/v1/mutes';
getNotifications = '/api/v1/notifications';
getSingleNotifications = '/api/v1/notifications/{0}';
clearNotifications = '/api/v1/notifications/clear';
getReports = '/api/v1/reports';
reportUser = '/api/v1/reports';
search = '/api/v1/search';
getStatus = '/api/v1/statuses/{0}';
getStatusContext = '/api/v1/statuses/{0}/context';
getStatusCard = '/api/v1/statuses/{0}/card';
getStatusRebloggedBy = '/api/v1/statuses/{0}/reblogged_by';
getStatusFavouritedBy = '/api/v1/statuses/{0}/favourited_by';
postNewStatus = '/api/v1/statuses';
deleteStatus = '/api/v1/statuses/{0}';
reblogStatus = '/api/v1/statuses/{0}/reblog';
unreblogStatus = '/api/v1/statuses/{0}/unreblog';
favouritingStatus = '/api/v1/statuses/{0}/favourite';
unfavouritingStatus = '/api/v1/statuses/{0}/unfavourite';
getHomeTimeline = '/api/v1/timelines/home';
getPublicTimeline = '/api/v1/timelines/public';
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 { BehaviorSubject } from "rxjs";
import { ApiRoutes } from "./models/api.settings";
import { StreamTypeEnum } from "../states/streams.state";
@Injectable()
export class StreamingService {
@ -9,12 +10,22 @@ export class StreamingService {
constructor() { }
//TODO restructure this to handle real domain objects
getStreaming(instance: string, accessToken: string, streamRequest: string): StreamingWrapper {
const route = `wss://${instance}/api/v1/streaming?access_token=${accessToken}&stream=${streamRequest}`
getStreaming(instance: string, accessToken: string, streamType: StreamTypeEnum): StreamingWrapper {
const request = this.getRequest(streamType);
const route = `wss://${instance}/api/v1/streaming?access_token=${accessToken}&stream=${request}`
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 {
@ -22,19 +33,37 @@ export class StreamingWrapper {
eventSource: WebSocket;
constructor(private readonly domain: string) {
this.start(domain);
this.start(domain);
}
private start(domain: string) {
this.eventSource = new WebSocket(domain);
this.eventSource.onmessage = x => this.tootParsing(<WebSocketEvent>JSON.parse(x.data));
this.eventSource.onerror = x => console.error(x);
this.eventSource.onmessage = x => this.statusParsing(<WebSocketEvent>JSON.parse(x.data));
this.eventSource.onerror = x => this.webSocketGotError(x);
this.eventSource.onopen = x => console.log(x);
this.eventSource.onclose = x => { console.log(x);
setTimeout(() => {this.start(domain)}, 3000);}
this.eventSource.onclose = x => this.webSocketClosed(domain, x);
}
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();
switch (event.event) {
@ -52,6 +81,8 @@ export class StreamingWrapper {
this.statusUpdateSubjet.next(newUpdate);
}
}
class WebSocketEvent {

View File

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