-
Notifications
You must be signed in to change notification settings - Fork 45
/
Copy pathPeriscopeCommentProvider2.cs
505 lines (473 loc) · 18.1 KB
/
PeriscopeCommentProvider2.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
using Codeplex.Data;
using Common;
using ryu_s.BrowserCookie;
using SitePlugin;
using SitePluginCommon;
using SitePluginCommon.AutoReconnection;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Net;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace PeriscopeSitePlugin
{
enum UrlType
{
Unknown,
LivePage,
Channel,
}
interface IUrl
{
UrlType Type { get; }
}
class ChannelUrl : IUrl
{
public UrlType Type { get; } = UrlType.Channel;
public string Url { get; }
public string ChannelId { get; }
public ChannelUrl(string channelUrl)
{
Url = channelUrl;
var (channelid, _) = Tools.ExtractChannelNameAndLiveId(channelUrl);
ChannelId = channelid;
}
}
class LivePageUrl : IUrl
{
public UrlType Type { get; } = UrlType.LivePage;
public string Url { get; }
public string LiveId { get; }
public string ChannelId { get; }
public LivePageUrl(string livePageUrl)
{
Url = livePageUrl;
var (channelid, liveid) = Tools.ExtractChannelNameAndLiveId(livePageUrl);
ChannelId = channelid;
LiveId = liveid;
}
}
class UnknownUrl : IUrl
{
public UrlType Type { get; } = UrlType.Unknown;
public string Url { get; }
public UnknownUrl(string url)
{
Url = url;
}
}
class MessageProvider : IProvider
{
private IWebsocket _websocket;
private readonly ILogger _logger;
public IProvider Master { get; }
public bool IsFinished { get; }
public Task Work { get; internal set; }
public ProviderFinishReason FinishReason { get; }
public string AccessToken { get; internal set; }
public string RoomId { get; internal set; }
public string WebsocketUrl { get; internal set; }
public event EventHandler<IInternalMessage> MessageReceived;
public event EventHandler<IMetadata> MetadataUpdated;
public void Start()
{
_websocket = new WebSocket(WebsocketUrl);
_websocket.Opened += Websocket_Opened;//weak referenceにしたい
_websocket.Received += Websocket_Received;//weak referenceにしたい
Work = _websocket.ReceiveAsync();
}
public void Stop()
{
_websocket?.Disconnect();
}
public MessageProvider(ILogger logger)
{
_logger = logger;
}
private void Websocket_Received(object sender, string e)
{
var raw = e;
Debug.WriteLine(raw);
try
{
var websocketMessage = MessageParser.ParseWebsocketMessage(raw);
var internalMessage = MessageParser.Parse(websocketMessage);
if (internalMessage != null)
{
MessageReceived?.Invoke(this, internalMessage);
}
}
catch (ParseException ex)
{
_logger.LogException(ex);
}
catch (Exception ex)
{
_logger.LogException(ex);
}
}
private async void Websocket_Opened(object sender, EventArgs e)
{
//{"payload":"{\"access_token\":\"\"}","kind":3}
//{"payload":"{\"body\":\"{\\\"room\\\":\\\"1MnxnvRQOAoxO\\\"}\",\"kind\":1}","kind":2}
await _websocket.SendAsync(CreateInitialMessage1(AccessToken));
await _websocket.SendAsync(CreateInitialMessage2(RoomId));
}
protected virtual string CreateInitialMessage1(string accessToken)
{
return "{\"payload\":\"{\\\"access_token\\\":\\\"" + accessToken + "\\\"}\",\"kind\":3}";
}
protected virtual string CreateInitialMessage2(string roomId)
{
return "{\"payload\":\"{\\\"body\\\":\\\"{\\\\\\\"room\\\\\\\":\\\\\\\"" + roomId + "\\\\\\\"}\\\",\\\"kind\\\":1}\",\"kind\":2}";
}
}
class TestAutoReconnector
{
private readonly ConnectionManager _connectionManager;
private readonly IDummy2 _dummy;
private readonly MessageUntara _messageUntara;
private readonly ILogger _logger;
private readonly IUrl _url;
private readonly IDataServer _server;
private bool IsBroadcastRunning(BroadcastInfo broadcastInfo)
{
return broadcastInfo.State == "RUNNING";
}
CancellationTokenSource _generateGroupCts;
public async Task AutoReconnectAsync()
{
_isDisconnectedByUser = false;
_generateGroupCts = new CancellationTokenSource();
while (true)
{
if (_url is UnknownUrl unknown)
{
_messageUntara.Set($"入力されたURLは無効です。({unknown.Url})", InfoType.Error);
break;
}
if (_url is ChannelUrl channelUrl)
{
//チャンネル
//配信中か確認
//配信中であればLiveIdを取得
//配信してなかったら始まるまで待機
try
{
var group = await _dummy.GenerateGroupAsync(_generateGroupCts.Token);
var reason = await _connectionManager.ConnectAsync(group);
System.Diagnostics.Debug.WriteLine($"接続が切れました。原因:{reason}");
}
catch (TaskCanceledException) { }
}
else if (_url is LivePageUrl livePageUrl)
{
var (_, broadcastInfo) = await Api.GetAccessVideoPublicAsync(_server, livePageUrl.LiveId);
if (!IsBroadcastRunning(broadcastInfo))
{
_messageUntara.Set($"この放送は終了しています。({livePageUrl.Url})", InfoType.Error);
break;
}
try
{
var group = await _dummy.GenerateGroupAsync(_generateGroupCts.Token);
var reason = await _connectionManager.ConnectAsync(group);
}
catch (TaskCanceledException) { }
//~の理由により接続が切断されました。
//配信が終了している場合でもcontinueでおk。
}
if (_isDisconnectedByUser)
{
break;
}
}
_generateGroupCts = null;
}
bool _isDisconnectedByUser;
public void Disconnect()
{
_isDisconnectedByUser = true;
_connectionManager.Disconnect();
_generateGroupCts?.Cancel();
}
public TestAutoReconnector(ConnectionManager connectionManager, IDummy2 dummy, MessageUntara messageUntara, ILogger logger, IUrl url, IDataServer server)
{
_connectionManager = connectionManager;
_dummy = dummy;
_messageUntara = messageUntara;
_logger = logger;
_url = url;
_server = server;
}
} /// <summary>
/// 名称未設定
/// </summary>
public interface IDummy2
{
Task<bool> CanConnectAsync();
Task<IEnumerable<IProvider>> GenerateGroupAsync(CancellationToken ct);
}
class DummyImpl : IDummy2
{
private readonly IDataServer _server;
private readonly IUrl _url;
private readonly IBrowserProfile _browserProfile;
private readonly ILogger _logger;
private readonly IPeriscopeSiteOptions _siteOptions;
private readonly MessageProvider _p1;
private readonly MessageUntara _messageSetter;
//private readonly MetadataProvider2 _p2;
public Task<bool> CanConnectAsync()
{
throw new NotImplementedException();
}
private bool IsBroadcastRunning(BroadcastInfo broadcastInfo)
{
return broadcastInfo.State == "RUNNING";
}
private async Task<string> WaitForBroadcastRunning(ChannelUrl channelUrl, CancellationToken ct)
{
string liveId;
while (true)
{
var html = await _server.GetAsync(channelUrl.Url);
var json = Tools.ExtractChannelPageJson(html);
var d = DynamicJson.Parse(json);
var broadcasts = ((object[])d.BroadcastCache.broadcasts);
if (broadcasts.Length > 0)
{
var broadcastJson = (string)((dynamic)((KeyValuePair<string, object>)broadcasts[0]).Value).broadcast.ToString();
var broadcast = Tools.Deserialize<Low.Broadcast.RootObject>(broadcastJson);
if (broadcast.State == "RUNNING")
{
liveId = broadcast.Id;
_messageSetter.Set($"{channelUrl.ChannelId}は配信中です。放送ID:{liveId}", InfoType.Debug);
break;
}
}
//待機する旨通知する。
_messageSetter.Set("配信していないため1分待機します。", InfoType.Notice);
await Task.Delay(60 * 1000, ct);//設定で変更できるようにする
}
return liveId;
}
/// <summary>
///
/// 配信してなかったら配信開始まで待機する。
/// </summary>
/// <returns></returns>
public async Task<IEnumerable<IProvider>> GenerateGroupAsync(CancellationToken ct)
{
string liveId;
if (_url is ChannelUrl channelUrl)
{
var a = await WaitForBroadcastRunning(channelUrl, ct);
liveId = a;
}
else if (_url is LivePageUrl livePageUrl)
{
liveId = livePageUrl.LiveId;
}
else
{
return new List<IProvider>();
}
var (avp, _) = await Api.GetAccessVideoPublicAsync(_server, liveId);
var acp = await Api.GetAccessChatPublicAsync(_server, avp.ChatToken);
var hostname = Tools.ExtractHostnameFromEndpoint(acp.Endpoint);
_p1.AccessToken = acp.AccessToken;
_p1.RoomId = liveId;
_p1.WebsocketUrl = $"wss://{hostname}/chatapi/v1/chatnow";
return new List<IProvider>
{
_p1,
};
}
public DummyImpl(IDataServer server, IUrl url, IBrowserProfile browserProfile, ILogger logger, IPeriscopeSiteOptions siteOptions, MessageProvider p1, MessageUntara messageSetter)
{
_server = server;
_url = url;
_browserProfile = browserProfile;
_logger = logger;
_siteOptions = siteOptions;
_p1 = p1;
_messageSetter = messageSetter;
//_p2 = p2;
}
}
class PeriscopeCommentProvider2 : CommentProviderBase
{
FirstCommentDetector _first = new FirstCommentDetector();
private readonly IDataServer _server;
private readonly ILogger _logger;
private readonly ICommentOptions _options;
private readonly IPeriscopeSiteOptions _siteOptions;
private readonly IUserStoreManager _userStoreManager;
public override async Task ConnectAsync(string input, IBrowserProfile browserProfile)
{
BeforeConnect();
try
{
await ConnectInternalAsync(input, browserProfile);
}
catch (Exception ex)
{
_logger.LogException(ex, "", $"input={input}");
}
finally
{
AfterDisconnected();
}
}
TestAutoReconnector _autoReconnector;
private async Task ConnectInternalAsync(string input, IBrowserProfile browserProfile)
{
var url = Tools.GetUrl(input);
if (url is UnknownUrl)
{
//不正なURL
return;
}
var cc = GetCookieContainer(browserProfile);
var messageSetter = new MessageUntara();
messageSetter.SystemInfoReiceved += MessageSetter_SystemInfoReiceved;
var p1 = new MessageProvider(_logger);
p1.MessageReceived += P1_MessageReceived;
p1.MetadataUpdated += P1_MetadataUpdated;
//var p2 = new MetadataProvider2(_server, _siteOptions);
//p2.MetadataUpdated += P2_MetadataUpdated;
//p2.Master = p1;
try
{
var dummy = new DummyImpl(_server, url, browserProfile, _logger, _siteOptions, p1, messageSetter);//, p2);
var connectionManager = new ConnectionManager(_logger);
_autoReconnector = new TestAutoReconnector(connectionManager, dummy, messageSetter, _logger, url, _server);
await _autoReconnector.AutoReconnectAsync();
}
finally
{
messageSetter.SystemInfoReiceved -= MessageSetter_SystemInfoReiceved;
p1.MessageReceived -= P1_MessageReceived;
p1.MetadataUpdated -= P1_MetadataUpdated;
//p2.MetadataUpdated -= P2_MetadataUpdated;
}
}
private void MessageSetter_SystemInfoReiceved(object sender, SitePluginCommon.AutoReconnector.SystemInfoEventArgs e)
{
SendSystemInfo(e.Message, e.Type);
}
private void P2_MetadataUpdated(object sender, ILiveInfo e)
{
}
private void P1_MetadataUpdated(object sender, IMetadata e)
{
}
private void P1_MessageReceived(object sender, IInternalMessage e)
{
if (e is Kind1Type1 kind1Type1)
{
var message = new PeriscopeComment(kind1Type1);
var userId = message.UserId;
var isFirstComment = _first.IsFirstComment(userId);
var user = GetUser(userId);
user.Name = MessagePartFactory.CreateMessageItems(message.Text);
var metadata = CreateMessageMetadata(message, user, isFirstComment);
var methods = new MessageMethods();
RaiseMessageReceived(new MessageContext(message, metadata, methods));
}
else if (e is Kind2Kind1 kind2kind1)
{
if (!_siteOptions.IsShowJoinMessage)
{
//取得する必要がないため無視する
return;
}
var message = new PeriscopeJoin(kind2kind1);
var userId = message.UserId;
var isFirstComment = false;
var user = GetUser(userId);
user.Name = MessagePartFactory.CreateMessageItems(message.DisplayName);
var metadata = CreateMessageMetadata(message, user, isFirstComment);
var methods = new MessageMethods();
RaiseMessageReceived(new MessageContext(message, metadata, methods));
}
else if (e is Kind2Kind2 kind2Kind2)
{
if (!_siteOptions.IsShowLeaveMessage)
{
//取得する必要がないため無視する
return;
}
var message = new PeriscopeLeave(kind2Kind2);
var userId = message.UserId;
var isFirstComment = false;
var user = GetUser(userId);
user.Name = MessagePartFactory.CreateMessageItems(message.DisplayName);
var metadata = CreateMessageMetadata(message, user, isFirstComment);
var methods = new MessageMethods();
RaiseMessageReceived(new MessageContext(message, metadata, methods));
}
}
private MessageMetadata CreateMessageMetadata(IPeriscopeMessage message, IUser user, bool isFirstComment)
{
return new MessageMetadata(message, _options, _siteOptions, user, this, isFirstComment)
{
SiteContextGuid = SiteContextGuid,
};
}
public override void Disconnect()
{
_autoReconnector?.Disconnect();
}
protected virtual CookieContainer GetCookieContainer(IBrowserProfile browserProfile)
{
var cc = new CookieContainer();
try
{
var cookies = browserProfile.GetCookieCollection("pscp.tv");
foreach (var cookie in cookies)
{
cc.Add(cookie);
}
}
catch (Exception ex)
{
_logger.LogException(ex);
}
return cc;
}
public override async Task<ICurrentUserInfo> GetCurrentUserInfo(IBrowserProfile browserProfile)
{
var userInfo = new CurrentUserInfo
{
};
return await Task.FromResult(userInfo);
}
public override IUser GetUser(string userId)
{
return _userStoreManager.GetUser(SiteType.Periscope, userId);
}
public override Task PostCommentAsync(string text)
{
throw new NotImplementedException();
}
public override void SetMessage(string raw)
{
throw new NotImplementedException();
}
public PeriscopeCommentProvider2(IDataServer server, ILogger logger, ICommentOptions options, IPeriscopeSiteOptions siteOptions, IUserStoreManager userStoreManager)
: base(logger, options)
{
_server = server;
_logger = logger;
_options = options;
_siteOptions = siteOptions;
_userStoreManager = userStoreManager;
}
}
}